package db

import (
	"center/db/models"
	"common/uuid"
	"common/version"
	"context"
	"fmt"
	"github.com/wuzfei/go-helper/maps"
	"github.com/wuzfei/go-helper/rand"
	"github.com/wuzfei/go-helper/slices"
	"go.uber.org/zap"
	"sort"
)

type weightFunc func(nodes *models.Node) float64

type ScoreNode struct {
	*models.Node
	Score float64
}

type ScoreNodes []*ScoreNode

func (s ScoreNodes) Len() int {
	return len(s)
}

// Less 比较索引为i的元素是否小于索引为j的元素
func (s ScoreNodes) Less(i, j int) bool {
	return s[i].Score > s[j].Score
}

// Swap 交换索引为 i 和 j 的元素。
func (s ScoreNodes) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

type NodeSelection struct {
	Quantity       int      //数量
	IncludeNodeId  [][]byte //指定某些
	ExcludeNodeId  [][]byte //排除指定某些
	ExcludeNodeIdx []uint32 //排除指定某些
	MinVersion     string   //最低版本
}

func (n NodeSelection) IncludeNodeIds() []uuid.UUID {
	res := make([]uuid.UUID, 0)
	if len(n.IncludeNodeId) == 0 {
		return res
	}
	for i := range n.IncludeNodeId {
		_uuid, _err := uuid.FromBytes(n.IncludeNodeId[i])
		if _err == nil {
			res = append(res, _uuid)
		}
	}
	return res
}

func (n NodeSelection) ExcludeNodeIds() []uuid.UUID {
	res := make([]uuid.UUID, 0)
	if len(n.ExcludeNodeId) == 0 {
		return res
	}
	for i := range n.ExcludeNodeId {
		_uuid, _err := uuid.FromBytes(n.ExcludeNodeId[i])
		if _err == nil {
			res = append(res, _uuid)
		}
	}
	return res
}

// GetUploadNodes 根据条件选择上传节点
func (d *CenterDB) GetUploadNodes(ctx context.Context, params *NodeSelection) (nodes []models.Node, err error) {
	nodes = make([]models.Node, 0)
	nodeIds := make([]uuid.UUID, 0)
	if params.Quantity == 0 {
		return nodes, nil
	}
	err = d.db.WithContext(ctx).Model(models.Node{}).Select("id").
		Where("fwd = 0 and disqualified is null and offline is null").Pluck("id", &nodeIds).Error
	if err != nil {
		return
	}

	//指定数量，补充数量
	var includeCount, limit int
	if len(nodeIds) <= params.Quantity {
		err = d.db.Find(&nodes, nodeIds).Error
	} else {
		//符合条件的数量大于需要数量
		needNodesId := make([]uuid.UUID, 0)
		includeIds := params.IncludeNodeIds()
		d.log.Debug("指定的nodeId", zap.Strings("includeIds", slices.Map(includeIds, func(v uuid.UUID, k int) string {
			return v.String()
		})))
		d.log.Debug("符合条件的所有nodeId", zap.Strings("allIds", slices.Map(nodeIds, func(v uuid.UUID, k int) string {
			return v.String()
		})))
		if len(includeIds) > 0 {
			needNodesId = slices.Intersect(includeIds, nodeIds)
			nodeIds = slices.Difference(includeIds, nodeIds)
			includeCount = len(needNodesId)
		}
		limit = params.Quantity - len(needNodesId)
		//如果指定的id比需求多，在去除两个，补充两个新的进去
		if len(needNodesId) >= params.Quantity {
			limit = 2
			needNodesId = needNodesId[:params.Quantity-2]
			includeCount = len(needNodesId)
		} else {
			needNodesId = append(needNodesId, rand.SlicesN(nodeIds, limit)...)
		}
		err = d.db.Find(&nodes, needNodesId).Error
	}
	d.log.Debug(fmt.Sprintf("获取节点参数：%v", params))
	d.log.Debug("获取节点数据", zap.Int("total", len(nodes)),
		zap.Int("includeCount", includeCount),
		zap.Int("newCount", limit),
	)
	return nodes, err
}
func (d *CenterDB) FindNodeWithByDevice(ctx context.Context, devId string) (*models.Node, error) {
	node := &models.Node{}
	err := d.db.WithContext(ctx).Model(models.Node{}).Where("dev_id = ?", devId).First(node).Error
	return node, err
}

// GetUploadNodesByWeight 根据条件选择上传节点
func (d *CenterDB) GetUploadNodesByWeight(ctx context.Context, params *NodeSelection) (nodes []*models.Node, err error) {
	nodes = make([]*models.Node, 0)
	if params.Quantity == 0 {
		return nodes, nil
	}

	d.log.Info("筛选节点条件",
		zap.Int("Quantity", params.Quantity),
		zap.Strings("IncludeIds", slices.Map(params.IncludeNodeIds(), func(v uuid.UUID, k int) string {
			return v.String()
		})),
		zap.Strings("ExcludeIds", slices.Map(params.ExcludeNodeIds(), func(v uuid.UUID, k int) string {
			return v.String()
		})),
		zap.Uint32s("ExcludeIdxs", params.ExcludeNodeIdx),
		zap.String("MinVersion", params.MinVersion),
	)
	ver, verErr := version.FromString(params.MinVersion)
	db := d.db.WithContext(ctx).Model(models.Node{}).
		Where("fwd = 0 and disqualified is null and offline is null")
	if verErr == nil {
		db = db.Where("version_int >= ?", ver.VerInt)
	}
	if len(params.ExcludeNodeId) > 0 {
		db = db.Where("id not in ?", params.ExcludeNodeId)
	} else if len(params.ExcludeNodeIdx) > 0 {
		db = db.Where("idx not in ?", params.ExcludeNodeIdx)
	}

	err = db.Debug().Order("RAND()").Limit(params.Quantity * 3).Find(&nodes).Error
	if err != nil {
		return
	}

	if len(nodes) < params.Quantity {
		d.log.Info("符合条件的节点 <所需,直接返回", zap.Int("nodes", len(nodes)))
		return
	}
	nodes = d.calNodeWeight(nodes, params.Quantity, params.IncludeNodeIds())
	return nodes, err
}

func (d *CenterDB) calNodeWeight(nodes []*models.Node, n int, includeIds []uuid.UUID) []*models.Node {
	diskFree := int64(0)
	vers := make([]uint32, 0)
	scoreNodes := make(ScoreNodes, len(nodes))
	for i := range nodes {
		diskFree += int64(nodes[i].FreeSpace)
		vers = append(vers, nodes[i].VersionInt)
		scoreNodes[i] = &ScoreNode{nodes[i], 0}
	}
	//按条件计算节点权重, 所占权重比例默认为1
	weightFuncs := map[string]weightFunc{
		"include":   nodeIncludeWeight(1, includeIds),
		"distSpace": nodeDiskSpaceWeight(1, diskFree),
		"version":   nodeVersionWeight(1, vers),
	}
	weightFuncKeys := maps.Keys(weightFuncs)
	for i := range scoreNodes {
		fields := make([]zap.Field, len(weightFuncKeys))
		for _i, k := range weightFuncKeys {
			_v := weightFuncs[k](scoreNodes[i].Node)
			scoreNodes[i].Score += _v
			fields[_i] = zap.Float64(k, _v)
		}
		_log := fmt.Sprintf("节点权重计算: node:%s, freeDisk:%d, version:%s score:%f",
			scoreNodes[i].ID, scoreNodes[i].FreeSpace, scoreNodes[i].Version, scoreNodes[i].Score)
		d.log.Info(_log, fields...)
	}
	//按积分排序，取高分的
	sort.Sort(scoreNodes)
	_nodeId := []string{}
	defer func() {
		d.log.Info("最终选中节点：", zap.Strings("nodes", _nodeId))
	}()
	return slices.Map(scoreNodes[:n], func(v *ScoreNode, k int) *models.Node {
		_nodeId = append(_nodeId, v.ID.String())
		return v.Node
	})
}

// nodeIncludeWeight 指定了，就得分加1
func nodeIncludeWeight(weight float64, include []uuid.UUID) weightFunc {
	return func(nodes *models.Node) float64 {
		if slices.Contains(include, nodes.ID) {
			return weight
		}
		return 0
	}
}

// nodeDiskSpaceWeight 按剩余空间占比，越高分越高
func nodeDiskSpaceWeight(weight float64, allDiskFree int64) weightFunc {
	return func(nodes *models.Node) float64 {
		return float64(nodes.FreeSpace) / float64(allDiskFree) * weight
	}
}

// nodeVersionWeight 按版本高低，越高分越高
func nodeVersionWeight(weight float64, vers []uint32) weightFunc {
	vers = slices.Unique(vers)
	sort.Slice(vers, func(i, j int) bool {
		return vers[i] < vers[j]
	})
	l := len(vers)
	verMap := make(map[uint32]int, l)
	if l >= 1 {
		for i := range vers {
			verMap[vers[i]] = i
		}
	}
	return func(nodes *models.Node) float64 {
		if l > 1 {
			if v, ok := verMap[nodes.VersionInt]; ok {
				return float64(v+1) / float64(l) * weight
			}
		}
		return float64(0)
	}
}
